<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Scripts\Cronds;

use Exception;
use Google\Cloud\Storage\StorageClient;
use Srv\Apps\Apis\Common\Ctl\StorageCtl;
use Srv\Apps\Apis\Common\DefCtl\DefStorageCategoryCtl;
use Srv\Conf\ConfModel\ConfExt;
use Srv\Conf\ConfModel\StorageConf;
use Srv\Libs\Boot;
use Srv\Libs\Common\CommArray;
use Srv\Libs\Common\CommFile;
use Srv\Libs\Common\CommTime;
use Srv\Libs\Daemon\PhpDaemon;
use Srv\Libs\Storage\DataMysql;
use Srv\Libs\Storage\DataRedis;
use Srv\Libs\Tools\ShellPrint;

require_once dirname(__DIR__, 2).'/Libs/Boot.php';
Boot::runConf('');
set_time_limit(0);

// 定时循环执行
(new class extends PhpDaemon
{
    protected string $defaultPidPath                = '';           // default pid file path
    protected string $defaultLogPath                = '';           // default log file path
    protected string $defaultConfPath               = '';           // default config file path
    protected string $defaultTmpPath                = '';           // default tmp file path
    protected int $defaultSleep                     = 3;            // default Sleep time
    private ?DataMysql $DbAuto                      = null;         // DB-Write
    private ?DataRedis $CcAuto                      = null;         // CcAuto

    /**
     *  constructor.
     */
    public function __construct()
    {
        $classFileName              = trim(preg_replace("'\\\\'", '_', __CLASS__), '_');
        (preg_match("/^class@anonymous\S+/i", $classFileName)) && $classFileName = basename(__FILE__);
        $pathLogs                   = Boot::getLogsPath();
        $this->defaultTmpPath       = Boot::getTmpPath();
        $this->defaultPidPath       = $pathLogs.'Cronds/'.$classFileName.'.pid';
        $this->defaultLogPath       = $pathLogs.'Cronds/'.$classFileName.'.log';
        $this->pidUrl               = $this->defaultPidPath;
        $this->logUrl               = $this->defaultLogPath;
        $this->confUrl              = $this->defaultConfPath;
        $this->sleep                = $this->defaultSleep;
        parent::__construct();
    }

    /**
     * runInit
     */
    public function runInit():void
    {
        $pId                        = getmypid();
        $this->DbAuto               = DataMysql::tag('auto', 'DBW_'.$pId);
        $this->CcAuto               = DataRedis::tag('auto', 'CC_'.$pId);
    }

    /**
     * run
     */
    public function run():void
    {
        ShellPrint::printlnString('-- Start -- '.CommTime::getString('Y-m-d H:i:s'));
        $splitTime      = 6;        // 切割时长
        $pageSize       = 10;       // 单次处理条数
        $StorageConf    = ConfExt::getStorageConfGroup()->getByGroupName(StorageCtl::GROUP_NAME_SGS_VOD);
        $listData       = $this->getSeriesList($pageSize);
        if(CommArray::isForeach($listData)){
            $skioIdList         = array_unique(array_column($listData, 'skioId'));
            $skitKeyList        = $this->getSkitKeyList($skioIdList);
            $skssIdListFail     = [];
            foreach ($listData as $item){
                $skssIdItem             = $item['skssId'];
                $skioIdItem             = $item['skioId'];
                $skitKeyItem            = $skitKeyList[$skioIdItem]??'';
                $seriesKeyItem          = $item['seriesKey'];
                $urlOriItem             = $item['urlOri'];
                $versionItem            = $item['version'] + 1;
                if(strlen($skitKeyItem) < 1 || strlen($seriesKeyItem) < 1){
                    $skssIdListFail[]   = $skssIdItem;
                    continue;
                }
                $m3u8SavePath           = '';
                $downloadDir            = '';
                $result                 = $this->mp4ToM3u8($urlOriItem, $skitKeyItem, $seriesKeyItem, $StorageConf, $versionItem, $m3u8SavePath, $downloadDir, $splitTime);
                ShellPrint::printlnString('-- mp4ToM3u8 result -- '.($result?'Success':'Fail'));
                if(!$result){
                    $skssIdListFail[]   = $skssIdItem;
                    continue;
                }
                $updateResult           = $this->updateSeriesUrl($skssIdItem, $m3u8SavePath, $versionItem);
                ShellPrint::printlnString('-- updateSeriesUrl -- '.($updateResult?'Success':'Fail'));
                if(!$updateResult) $skssIdListFail[] = $skssIdItem;
                if(is_dir($downloadDir) && $downloadDir != '/' && str_contains($downloadDir, '/Data/Tmp/')){
                    ShellPrint::printlnString('-- rm -rf '.$downloadDir.'*.ts *.m3u8 *.mp4');
                    shell_exec("rm -rf ".$downloadDir.'*.ts');
                    shell_exec("rm -rf ".$downloadDir.'*.m3u8');
                    shell_exec("rm -rf ".$downloadDir.'*.mp4');
                }
            }
            if(count($skssIdListFail) > 0){
                $this->DbAuto->getDataNum('UPDATE '.$this->DbAuto->getTableNameAll('skit_series').' SET skss_url_handle=3 WHERE skss_id IN('.implode(',', $skssIdListFail).') AND skss_delete=0 AND skss_url_handle=2');
            }
        }
        ShellPrint::printlnString('-- End -- '.CommTime::getString('Y-m-d H:i:s'));
        exit();
    }

    /**
     * @param int $skssId
     * @param string $url
     * @param int $versionNum
     * @return bool
     * updateSeriesUrl
     */
    private function updateSeriesUrl(int $skssId, string $url, int $versionNum):bool
    {
        if($skssId < 1 || strlen($url) < 1 || $versionNum < 0) return false;
        $sql    = 'UPDATE '.$this->DbAuto->getTableNameAll('skit_series').' SET skss_url=\''.$this->DbAuto->getRealEscapeString($url).'\', skss_version='.$versionNum.', skss_url_handle=1 WHERE skss_id='.$skssId.' AND skss_url_handle=2 AND skss_delete=0';
        return $this->DbAuto->getDataNum($sql) > 0;
    }

    /**
     * @param array $skioIdList
     * @return array
     * getSkitKeyList
     */
    private function getSkitKeyList(array $skioIdList):array{
        $listData       = [];
        if(count($skioIdList) < 1) return $listData;
        $sql            = 'SELECT skio_id, skio_key FROM '.$this->DbAuto->getTableNameAll('skit_info').' WHERE skio_id IN('.implode(',',$skioIdList).') AND skio_delete=0';
        $list           = $this->DbAuto->getData($sql);
        if(CommArray::isForeach($list)){
            foreach ($list  as $item){
                $skioIdItem             = intval($item['skio_id']??0);
                if($skioIdItem < 1) continue;
                $listData[$skioIdItem]  = $item['skio_key'];
            }
        }
        return $listData;
    }

    /**
     * @param int $size
     * @return array
     * 获取要切片的剧集列表
     */
    public function getSeriesList(int $size):array
    {
        $listData           = [];
        if($size < 1) return $listData;
        $sql                = 'SELECT skss_id, skss_key, skio_id, skss_url_ori, skss_version FROM '.$this->DbAuto->getTableNameAll('skit_series').' WHERE skss_delete=0 AND skss_url_handle=0 ORDER BY skss_id ASC LIMIT '.$size;
        $list               = $this->DbAuto->getData($sql);
        $skssIdListFail     = [];
        $skssIdListSuccess  = [];
        foreach ($list as $item){
            $skssIdItem             = intval($item['skss_id']??0);
            if($skssIdItem < 1) continue;
            $urlOriItem             = $item['skss_url_ori'];
            $skioIdItem             = intval($item['skio_id']);
            $seriesKeyItem          = $item['skss_key'];
            $versionItem            = intval($item['skss_version']);
            if(strlen($urlOriItem) < 5 || strlen($seriesKeyItem) < 1){
                $skssIdListFail[]   = $skssIdListFail;
                continue;
            }
            $skssIdListSuccess[]    = $skssIdItem;
            $listData[]             = [
                'skssId'            => $skssIdItem,
                'skioId'            => $skioIdItem,
                'seriesKey'         => $seriesKeyItem,
                'urlOri'            => $urlOriItem,
                'version'           => $versionItem,
            ];
        }
        if(count($skssIdListFail) > 0){
            $resNum     = $this->DbAuto->getDataNum('UPDATE '.$this->DbAuto->getTableNameAll('skit_series').' SET skss_url_handle=3 WHERE skss_id IN('.implode(',', $skssIdListFail).') AND skss_delete=0 AND skss_url_handle=0');
            if($resNum < 1) {
                return [];
            }
        }
        if(count($skssIdListSuccess) > 0){
            $resNum     = $this->DbAuto->getDataNum('UPDATE '.$this->DbAuto->getTableNameAll('skit_series').' SET skss_url_handle=2 WHERE skss_id IN('.implode(',', $skssIdListSuccess).') AND skss_delete=0 AND skss_url_handle=0');
            if($resNum < 1) {
                return [];
            }
        }
        return $listData;
    }

    /**
     * @param string $mp4Url
     * @param string $skitKey
     * @param string $seriesKey
     * @param StorageConf $StorageConf
     * @param int $versionNum
     * @param string $m3u8SavePath
     * @param string $downloadDir
     * @param int $splitTime
     * @return bool
     * mp4ToM3u8
     */
    private function mp4ToM3u8(string $mp4Url, string $skitKey, string $seriesKey, StorageConf $StorageConf, int $versionNum, string &$m3u8SavePath, string &$downloadDir, int $splitTime=6):bool
    {
        if(strlen($mp4Url) < 1 || strlen($skitKey) < 1 || strlen($seriesKey) < 1) return false;
        // exec("ffmpeg -i 11.mp4 -c copy output.ts",$out,$status); mp4先转换成ts
        // exec("ffmpeg -i output.ts -c copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 5 output%03d.ts ",$out,$status); 将ts文件切片
        $pathPrefix         = DefStorageCategoryCtl::tag()->getExtStrByKey(DefStorageCategoryCtl::KEY_VIDEO_RES, DefStorageCategoryCtl::EXT_PATH_PREFIX);
        if(strlen($pathPrefix) < 1) return false;
        $currTime           = CommTime::getTimeStamp(0);
        $uploadDir          = $pathPrefix.'/'.CommTime::getString('ymd', $currTime).'/'.$skitKey.'/'.$seriesKey.'/720/'.$versionNum.'/';    // 上传目录
        $m3u8FileName       = 'mixed.m3u8'; // m3u8命名
        $downloadDir        = $this->defaultTmpPath.$skitKey.'/'.$seriesKey.'/';   // 下载目录
        $downloadFileName   = $this->getRemoteUrlFileName($mp4Url);
        $downloadUrl        = $downloadDir.$downloadFileName;
        $m3u8LocalUrl       = $downloadDir.$m3u8FileName;
        $m3u8UploadUrl      = $uploadDir.$m3u8FileName;
        $mp4RemoteUrl       = $this->genPlayUrl($mp4Url);
        if(strlen($mp4RemoteUrl) < 5) return false;
        // 删除原有的文件
        if(CommFile::isExistsFile($downloadUrl)) CommFile::deleteFile($downloadUrl);    // 下载文件存在就删除
        if(CommFile::isExistsFile($m3u8LocalUrl)) CommFile::deleteFile($m3u8LocalUrl);  // m3u8文件存在就删除
        // 清空目录中的ts文件
        if(is_dir($downloadDir)){
            $lsTsExecStr        = sprintf("ls -al %s | grep '^-' | wc -l ", $downloadDir.'*.ts');
            ShellPrint::printlnString('-- '.$lsTsExecStr);
            $tsNum              = intval(shell_exec($lsTsExecStr)??0);
            ShellPrint::printlnString('-- '.$tsNum);
            if($tsNum > 0) {
                $deleteTsExecStr    = sprintf("rm -rf %s 2>&1", $downloadDir.'*.ts');
                ShellPrint::printlnString('-- '.$deleteTsExecStr);
                shell_exec($deleteTsExecStr);
            }
        }
        // 下载文件
        $downloadContent    = '';
        if(!CommFile::getContentNet($mp4RemoteUrl, [], $downloadContent)) return false;
        if(!CommFile::writeFileString($downloadUrl, $downloadContent)) return false;
        // 判断下载文件是否存在
        if(!file_exists($downloadUrl) || filesize($downloadUrl) < 2048) return false;
        $tsTempName         = sprintf("%s_%s.ts", $skitKey, $seriesKey);
        $tsTempLocal        = $downloadDir.$tsTempName;
        $transExecStr       = sprintf("ffmpeg -v 0 -y -i %s -c copy %s 2>&1", $downloadUrl, $tsTempLocal);
        ShellPrint::printlnString('-- '.$transExecStr);
        shell_exec($transExecStr);
        // 判断ts文件是否存在
        if(!file_exists($tsTempLocal) || filesize($tsTempLocal) < 2048) return false;
        $m3u8PieceName      = $downloadDir.$seriesKey.'_'.$versionNum."_%6d.ts";
        $segExecStr         = sprintf("ffmpeg -v 0 -y -i %s -c copy -map 0 -f segment -segment_list %s -segment_time %d %s 2>&1", $tsTempLocal, $m3u8LocalUrl, $splitTime, $m3u8PieceName);
        ShellPrint::printlnString('-- '.$segExecStr);
        shell_exec($segExecStr);
        // 判断m3u8文件是否存在以及切片文件是否存在
        ShellPrint::printlnString('-- checkMu3u8File');
        $uploadList         = [];   // 上传对象列表 [['localPath' => '', savePath => '']]
        if(!$this->checkM3u8File($m3u8LocalUrl, $downloadDir, $uploadDir, $uploadList) || count($uploadList) < 1) return false;
        // upload dir
        ShellPrint::printlnString('-- saveStorageFileForGsPath');
        // 循环 uploadList 一个个上传
        $uploadList[]       = ['localPath' => $m3u8LocalUrl, 'savePath' => $m3u8UploadUrl];
        if(!$this->uploadM3u8Batch($uploadList, $StorageConf)) return false;
        $m3u8SavePath       = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$m3u8UploadUrl; // 远程保存地址
        return true;
    }

    /**
     * @param string $url
     * @return string
     * getRemoteUrlFileName
     */
    private function getRemoteUrlFileName(string $url):string
    {
        if(strlen($url) < 1) return '';
        $urlArr     = parse_url($url);
        if(count($urlArr) < 1) return '';
        $pathArr    = pathinfo($urlArr['path']);
        return $pathArr['basename']??'';
    }

    /**
     * @param string $playUrlSrc
     * @return string
     * genPlayUrl
     */
    private function genPlayUrl(string $playUrlSrc):string
    {
        if(strlen($playUrlSrc) < 1) return '';
        $StorageConf        = null;
        $StorageCtl         = StorageCtl::tag();
        $playUrl            = $StorageCtl->getStorageUrlFile($playUrlSrc, true, $StorageConf);
        $path               = '';
        $bucketName         = '';
        $appId              = '';
        if($StorageCtl->formatStorageUrl($playUrlSrc, $path, $bucketName, $appId) && ($StorageConf instanceof StorageConf)){
            try{
                $StorageClient  = new StorageClient(['keyFilePath' => $StorageConf->getSecretKeyFile()]);
                $ttl            = $StorageConf->getFirebaseCustomTokenTtl();
                if($ttl < 1) $ttl = 3600;
                return $StorageClient->bucket($StorageConf->getAppName())->object(ltrim($path, '/'))->signedUrl(CommTime::getTimeStamp() + $ttl, ['method' => 'GET', 'version' => 'v4']);
            }catch (Exception $Exception){
                return $playUrl;
            }
        }
        return $playUrl;
    }

    /**
     * @param string $m3u8LocalUrl
     * @param string $downloadDir
     * @param string $uploadDir
     * @param array $uploadList
     * @return bool
     * 检查m3u8文件及切片文件是否存在且完整
     */
    private function checkM3u8File(string $m3u8LocalUrl, string $downloadDir, string $uploadDir, array &$uploadList):bool
    {
        if(!file_exists($m3u8LocalUrl) || filesize($m3u8LocalUrl) < 8 || strlen($uploadDir) < 1) return false;
        $m3u8Content        = file_get_contents($m3u8LocalUrl);
        $matches            = [];
        if(!preg_match_all("/\n(.+?.ts)\n/", $m3u8Content, $matches)) return false;
        if(!isset($matches[1]) || count($matches[1]) < 1) return false;
        foreach ($matches[1] as $item){
            $tsLocalUrl     = $downloadDir.$item;
            if(!file_exists($tsLocalUrl) || filesize($tsLocalUrl) < 2048){
                $uploadList = [];
                return false;
            }
            $uploadList[]   = [
                'localPath' => $tsLocalUrl,
                'savePath'  => $uploadDir.$item
            ];
        }
        return true;
    }

    /**
     * @param array $uploadList
     * @param StorageConf $StorageConf
     * @return bool
     * uploadM3u8Batch
     */
    private function uploadM3u8Batch(array $uploadList, StorageConf $StorageConf):bool
    {
        if(count($uploadList) < 1) return false;
        foreach ($uploadList as $item){
            $localPath      = $item['localPath'];
            $savePath       = $item['savePath'];
            if(strlen($localPath) < 1 || !file_exists($localPath)){
                return false;
            }
            $remotePath     = '';
            if(!StorageCtl::tag()->saveStorageFileForGsPath($savePath, $localPath, $StorageConf, $remotePath) || strlen($remotePath) < 1) return false;
        }
        return true;
    }
});

